package com.google.code.stalkernet.message;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Arrays;

import com.google.code.stalkernet.advertisement.Advertisement;
import com.google.code.stalkernet.advertisement.AdvertisementUtils;
import com.google.code.stalkernet.entry.Entry;
import com.google.code.stalkernet.entry.EntryBody;
import com.google.code.stalkernet.entry.EntryID;
import com.google.code.stalkernet.entry.EntryUtils;
import com.google.code.stalkernet.node.NodeID;
import com.google.code.stalkernet.node.NodeUtils;

/**
 * Contains various useful methods to work with messages.
 *
 * @author Mikhail Vladimirov
 */
public class MessageUtils
{
    private final static byte NO_MESSAGE = 0;
    private final static byte LEAF_IDS = 1;
    private final static byte LEAD_IDS_ACK = 2;
    private final static byte ENTRIES_REQUEST = 3;
    private final static byte ENTRIES = 4;
    private final static byte ADVERTISEMENTS = 5;
    private final static byte ADVERTISEMENTS_ACK = 6;

    private MessageUtils ()
    {
        throw new Error ("Do not instantiate me");
    }

    /**
     * Create {@link LeafIDsMessage} with given IDs of leaf entries.
     *
     * @param leafIDs IDs of leaf entries
     * @return {@link LeafIDsMessage} object
     */
    public static LeafIDsMessage createLeafIDsMessage (EntryID [] leafIDs)
    {
        if (leafIDs == null)
            throw new IllegalArgumentException ("Leaf IDs is null");

        return new MyLeafIDsMessage (leafIDs);
    }

    /**
     * Create {@link LeafIDsAckMessage}.
     *
     * @return {@link LeafIDsAckMessage} object
     */
    public static LeafIDsAckMessage createLeafIDsAckMessage ()
    {
        return MyLeafIDsAckMessage.INSTANCE;
    }

    /**
     * Create {@link EntriesRequestMessage} with given IDs of requested entries.
     *
     * @param entryIDs IDs of requested entries
     * @return {@link EntriesRequestMessage} object
     */
    public static EntriesRequestMessage createEntriesRequestMessage (
        EntryID [] entryIDs)
    {
        if (entryIDs == null)
            throw new IllegalArgumentException ("Entry IDs is null");

        return new MyEntriesRequestMessage (entryIDs);
    }

    /**
     * Create {@link EntriesMessage} with given entries.
     *
     * @param entries entries
     * @return {@link EntriesMessage} object
     */
    public static EntriesMessage createEntriesMessage (Entry [] entries)
    {
        if (entries == null)
            throw new IllegalArgumentException ("Entries is null");

        return new MyEntriesMessage (entries);
    }

    /**
     * Create {@link AdvertisementsMessage} with given entries.
     *
     * @param advertisements advertisements
     * @return {@link AdvertisementsMessage} object
     */
    public static AdvertisementsMessage createAdvertisementsMessage (
        Advertisement [] advertisements)
    {
        if (advertisements == null)
            throw new IllegalArgumentException (
                "Advertisements is null");

        return new MyAdvertisementsMessage (advertisements);
    }

    /**
     * Create {@link AdvertisementsAckMessage} with given entries.
     *
     * @return {@link AdvertisementsAckMessage} object
     */
    public static AdvertisementsAckMessage
        createAdvertisementsAckMessage ()
    {
        return MyPeersAdvertisementAckMessage.INSTANCE;
    }

    /**
     * Format given {@link Message} as human-readable string.
     *
     * @param message {@link Message} to be formatted
     * @return message formatted as human-readable string
     */
    public static String formatMessage (Message message)
    {
        if (message == null)
            throw new IllegalArgumentException ("Message is null");

        return message.accept (FormattingMessageVisitor.INSTANCE);
    }

    /**
     * Marshal given {@link Message} to given {@link DataOutput}.
     *
     * @param message {@link Message} to marshal
     * @param dataOutput {@link DataOutput} to marshal message to
     * @throws IOException if I/O error occurred
     */
    public static void marshalMessage (Message message, DataOutput dataOutput)
        throws IOException
    {
        if (message == null)
            throw new IllegalArgumentException ("Message is null");

        if (dataOutput == null)
            throw new IllegalArgumentException ("Data output is null");

        try
        {
            message.accept (new MarshallingMessageVisitor (dataOutput));
        }
        catch (IOExceptionWrapper ex)
        {
            throw ex.getIOException ();
        }
    }

    /**
     * Unmarshal {@link Message} from given {@link DataInput}.
     *
     * @param dataInput {@link DataInput} to unmarshal message from
     * @return unmarshalled {@link Message}
     * @throws IOException if I/O error occurred
     */
    public static Message unmarshalMessage (DataInput dataInput)
        throws IOException
    {
        if (dataInput == null)
            throw new IllegalArgumentException ("Data input is null");

        byte messageType;

        do
        {
            messageType = dataInput.readByte ();
        }
        while (messageType == NO_MESSAGE);

        switch (messageType)
        {
        case LEAF_IDS:
            int leafIDsCount = dataInput.readInt ();
            EntryID [] leafIDs = new EntryID [leafIDsCount];
            for (int i = 0; i < leafIDsCount; i++)
            {
                int bytesCount = dataInput.readInt ();
                byte [] bytes = new byte [bytesCount];
                dataInput.readFully (bytes);
                leafIDs [i] = EntryUtils.createEntryID (bytes);
            }

            return createLeafIDsMessage (leafIDs);
        case LEAD_IDS_ACK:
            return createLeafIDsAckMessage ();
        case ENTRIES_REQUEST:
            int entryIDsCount = dataInput.readInt ();
            EntryID [] entryIDs = new EntryID [entryIDsCount];
            for (int i = 0; i < entryIDsCount; i++)
            {
                int bytesCount = dataInput.readInt ();
                byte [] bytes = new byte [bytesCount];
                dataInput.readFully (bytes);
                entryIDs [i] = EntryUtils.createEntryID (bytes);
            }

            return createEntriesRequestMessage (entryIDs);
        case ENTRIES:
            int entriesCount = dataInput.readInt ();
            Entry [] entries = new Entry [entriesCount];
            for (int i = 0; i < entriesCount; i++)
            {
                int idBytesCount = dataInput.readInt ();
                byte [] idBytes = new byte [idBytesCount];
                dataInput.readFully (idBytes);
                EntryID id = EntryUtils.createEntryID (idBytes);

                int bodyBytesCount = dataInput.readInt ();
                byte [] bodyBytes = new byte [bodyBytesCount];
                dataInput.readFully (bodyBytes);
                EntryBody body = EntryUtils.createEntryBody (bodyBytes);

                int parentIDsCount = dataInput.readInt ();
                EntryID [] parentIDs = new EntryID [parentIDsCount];
                for (int j = 0; j < parentIDsCount; j++)
                {
                    int bytesCount = dataInput.readInt ();
                    byte [] bytes = new byte [bytesCount];
                    dataInput.readFully (bytes);
                    parentIDs [j] = EntryUtils.createEntryID (bytes);
                }

                entries [i] = EntryUtils.createEntry (
                    id, body, parentIDs);
            }

            return createEntriesMessage (entries);
        case ADVERTISEMENTS:
            int advertisementsCount = dataInput.readInt ();
            Advertisement [] advertisements = new
                Advertisement [advertisementsCount];
            for (int i = 0; i < advertisementsCount; i++)
            {
                int inetAddressBytesCount = dataInput.readInt ();
                byte [] inetAddressBytes =
                    new byte [inetAddressBytesCount];
                dataInput.readFully (inetAddressBytes);
                InetAddress inetAddress =
                    InetAddress.getByAddress (inetAddressBytes);
                int port = dataInput.readInt ();
                int nodeIDBytesCount = dataInput.readInt ();
                byte [] nodeIDBytes = new byte [nodeIDBytesCount];
                dataInput.readFully (nodeIDBytes);
                advertisements [i] =
                    AdvertisementUtils.createAdvertisement (
                        new InetSocketAddress (inetAddress, port),
                        NodeUtils.createNodeID (nodeIDBytes));
            }

            return MessageUtils.createAdvertisementsMessage (advertisements);
        case ADVERTISEMENTS_ACK:
            return MessageUtils.createAdvertisementsAckMessage ();
        default:
            throw new IllegalArgumentException (
                "Unknown message type: " + messageType);
        }
    }

    private static class MyLeafIDsMessage extends AbstractLeafIDsMessage
    {
        private final EntryID [] leafIDs;

        public MyLeafIDsMessage (EntryID [] leafIDs)
        {
            if (leafIDs == null)
                throw new IllegalArgumentException ("Leaf IDs is null");

            this.leafIDs = leafIDs.clone ();
        }

        @Override
        public EntryID [] getLeafIDs ()
        {
            return leafIDs.clone ();
        }
    }

    private static class MyLeafIDsAckMessage extends AbstractLeafIDsAckMessage
    {
        public final static MyLeafIDsAckMessage INSTANCE =
            new MyLeafIDsAckMessage ();

        private MyLeafIDsAckMessage ()
        {
        }
    }

    private static class MyEntriesRequestMessage
        extends AbstractEntriesRequestMessage
    {
        private final EntryID [] entryIDs;

        public MyEntriesRequestMessage (EntryID [] entryIDs)
        {
            if (entryIDs == null)
                throw new IllegalArgumentException ("Entry IDs is null");

            this.entryIDs = entryIDs.clone ();
        }

        @Override
        public EntryID [] getEntryIDs ()
        {
            return entryIDs.clone ();
        }
    }

    private static class MyEntriesMessage extends AbstractEntriesMessage
    {
        private final Entry [] entries;

        public MyEntriesMessage (Entry [] entries)
        {
            if (entries == null)
                throw new IllegalArgumentException ("Entries is null");

            this.entries = entries.clone ();
        }

        @Override
        public Entry [] getEntries ()
        {
            return entries.clone ();
        }
    }

    private static class MyAdvertisementsMessage
        extends AbstractAdvertisementsMessage
    {
        private final Advertisement [] advertisements;

        public MyAdvertisementsMessage (Advertisement [] advertisements)
        {
            if (advertisements == null)
                throw new IllegalArgumentException (
                    "Advertisements is null");

            this.advertisements = advertisements.clone ();
        }

        @Override
        public Advertisement [] getAdvertisements ()
        {
            return advertisements.clone ();
        }
    }

    private static class MyPeersAdvertisementAckMessage
        extends AbstractAdvertisementsAckMessage
    {
        public final static MyPeersAdvertisementAckMessage INSTANCE =
            new MyPeersAdvertisementAckMessage ();

        private MyPeersAdvertisementAckMessage ()
        {
            // Nothing here
        }
    }

    private static class FormattingMessageVisitor
        implements MessageVisitor <String>
    {
        public final static FormattingMessageVisitor INSTANCE =
            new FormattingMessageVisitor ();

        private FormattingMessageVisitor ()
        {
            // Do nothing
        }

        @Override
        public String visitLeafIDsMessage (LeafIDsMessage leafIDsMessage)
        {
            if (leafIDsMessage == null)
                throw new IllegalArgumentException ("Leaf IDs message is null");

            return "LeafIDsMessage {leafIDs: " +
                Arrays.toString (leafIDsMessage.getLeafIDs ())+ "}";
        }

        @Override
        public String visitLeafIDsAckMessage (
            LeafIDsAckMessage leafIDsAckMessage)
        {
            if (leafIDsAckMessage == null)
                throw new IllegalArgumentException (
                    "Leaf IDs ack message is null");

            return "LeafIDsAckMessage";
        }

        @Override
        public String visitEntriesRequestMessage (
            EntriesRequestMessage entriesRequestMessage)
        {
            if (entriesRequestMessage == null)
                throw new IllegalArgumentException (
                    "Entries request message is null");

            return "EntriesRequestMessage {entryIDs: " +
                Arrays.toString (entriesRequestMessage.getEntryIDs ())+ "}";
        }

        @Override
        public String visitEntriesMessage (EntriesMessage entriesMessage)
        {
            if (entriesMessage == null)
                throw new IllegalArgumentException (
                    "Entries message is null");

            return "EntriesMessage {entries: " +
                Arrays.toString (entriesMessage.getEntries ())+ "}";
        }

        @Override
        public String visitAdvertisementsMessage (
            AdvertisementsMessage advertisementMessage)
        {
            if (advertisementMessage == null)
                throw new IllegalArgumentException (
                    "Advertisements message is null");

            return "AdvertisementsMessage {advertisements: " +
                Arrays.toString (
                    advertisementMessage.getAdvertisements ()
                ) + "}";
        }

        @Override
        public String visitAdvertisementsAckMessage (
            AdvertisementsAckMessage advertisementsAckMessage)
        {
            if (advertisementsAckMessage == null)
                throw new IllegalArgumentException (
                    "Advertisement ack message is null");

            return "AdvertisementsAckMessage";
        }
    }

    private static class MarshallingMessageVisitor
        implements MessageVisitor <Void>
    {
        private final DataOutput dataOutput;

        public MarshallingMessageVisitor (DataOutput dataOutput)
        {
            if (dataOutput == null)
                throw new IllegalArgumentException ("Data output is null");

            this.dataOutput = dataOutput;
        }

        @Override
        public Void visitLeafIDsMessage (LeafIDsMessage leafIDsMessage)
        {
            if (leafIDsMessage == null)
                throw new IllegalArgumentException ("Leaf IDs message is null");

            try
            {
                dataOutput.writeByte (LEAF_IDS);
                EntryID [] leafIDs = leafIDsMessage.getLeafIDs ();
                dataOutput.writeInt (leafIDs.length);
                for (EntryID leafID: leafIDs)
                {
                    byte [] bytes = leafID.getBytes ();

                    dataOutput.writeInt (bytes.length);
                    dataOutput.write (bytes);
                }
            }
            catch (IOException ex)
            {
                throw new IOExceptionWrapper (ex);
            }

            return null;
        }

        @Override
        public Void visitLeafIDsAckMessage (
            LeafIDsAckMessage leafIDsAckMessage)
        {
            if (leafIDsAckMessage == null)
                throw new IllegalArgumentException (
                    "Leaf IDs ack message is null");

            try
            {
                dataOutput.writeByte (LEAD_IDS_ACK);
            }
            catch (IOException ex)
            {
                throw new IOExceptionWrapper (ex);
            }

            return null;
        }

        @Override
        public Void visitEntriesRequestMessage (
            EntriesRequestMessage entriesRequestMessage)
        {
            if (entriesRequestMessage == null)
                throw new IllegalArgumentException (
                    "Entries request message is null");

            try
            {
                dataOutput.writeByte (ENTRIES_REQUEST);
                EntryID [] entryIDs = entriesRequestMessage.getEntryIDs ();
                dataOutput.writeInt (entryIDs.length);
                for (EntryID entryID: entryIDs)
                {
                    byte [] bytes = entryID.getBytes ();

                    dataOutput.writeInt (bytes.length);
                    dataOutput.write (bytes);
                }
            }
            catch (IOException ex)
            {
                throw new IOExceptionWrapper (ex);
            }

            return null;
        }

        @Override
        public Void visitEntriesMessage (EntriesMessage entriesMessage)
        {
            if (entriesMessage == null)
                throw new IllegalArgumentException ("Entries message is null");

            try
            {
                dataOutput.writeByte (ENTRIES);
                Entry [] entries = entriesMessage.getEntries ();
                dataOutput.writeInt (entries.length);
                for (Entry entry: entries)
                {
                    byte [] idBytes = entry.getID ().getBytes ();
                    dataOutput.writeInt (idBytes.length);
                    dataOutput.write (idBytes);

                    byte [] bodyBytes = entry.getBody ().getBytes ();
                    dataOutput.writeInt (bodyBytes.length);
                    dataOutput.write (bodyBytes);

                    EntryID [] parentIDs = entry.getParentIDs ();
                    dataOutput.writeInt (parentIDs.length);
                    for (EntryID parentID: parentIDs)
                    {
                        byte [] bytes = parentID.getBytes ();

                        dataOutput.writeInt (bytes.length);
                        dataOutput.write (bytes);
                    }
                }
            }
            catch (IOException ex)
            {
                throw new IOExceptionWrapper (ex);
            }

            return null;
        }

        @Override
        public Void visitAdvertisementsMessage (
            AdvertisementsMessage advertisemenetsMessage)
        {
            if (advertisemenetsMessage == null)
                throw new IllegalArgumentException (
                    "Advertisements message is null");

            try
            {
                dataOutput.writeByte (ADVERTISEMENTS);
                Advertisement [] advertisements =
                    advertisemenetsMessage.getAdvertisements ();
                dataOutput.writeInt (advertisements.length);
                for (Advertisement advertisement: advertisements)
                {
                    InetSocketAddress inetSocketAddress =
                        (InetSocketAddress)advertisement.getSocketAddress ();

                    InetAddress inetAddress = inetSocketAddress.getAddress ();
                    byte [] inetAddressBytes = inetAddress.getAddress ();
                    dataOutput.writeInt (inetAddressBytes.length);
                    dataOutput.write (inetAddressBytes);

                    dataOutput.writeInt (inetSocketAddress.getPort ());

                    NodeID nodeID = advertisement.getNodeID ();
                    byte [] nodeIDBytes = nodeID.getBytes ();
                    dataOutput.writeInt (nodeIDBytes.length);
                    dataOutput.write (nodeIDBytes);
                }
            }
            catch (IOException ex)
            {
                throw new IOExceptionWrapper (ex);
            }

            return null;
        }

        @Override
        public Void visitAdvertisementsAckMessage (
            AdvertisementsAckMessage advertisementsAckMessage)
        {
            if (advertisementsAckMessage == null)
                throw new IllegalArgumentException (
                    "Advertisements ack message is null");

            try
            {
                dataOutput.writeByte (ADVERTISEMENTS_ACK);
            }
            catch (IOException ex)
            {
                throw new IOExceptionWrapper (ex);
            }

            return null;
        }
    }

    private static class IOExceptionWrapper extends RuntimeException
    {
        private final IOException ioException;

        public IOExceptionWrapper (IOException ioException)
        {
            if (ioException == null)
                throw new IllegalArgumentException ("IOException is null");

            this.ioException = ioException;
        }

        public IOException getIOException ()
        {
            return ioException;
        }
    }
}
